這篇接續討論上篇沒討論到的類別。
算術運算子除了基本的加減乘除外,還有 %
,++
,--
,-
(負號),+
(正號),**
,
字串運算子只有一個 +
。
有經驗的開發者對這些運算子的個別用途應該都理解了,我就不對個別作用做說明,而是提一下使用時需要注意的地方。
++
,--
)自增和自減運算子有個比較特別的東西,在其他語言也會看到:他們都是一元運算子,同時,他可以被放在運算元的前面或後面。(前置或後置,指的是運算子放的位置,++a 稱作前置)
let a = 0;
console.log(a++);//0
console.log(++a);//2
let b = 0;
console.log(b--);//0
console.log(--b);//-2
在無需立刻取值的情況下,前置後置並無差別。
放置位置會決定他的執行順序。
但需要取值的時候,前置會先運算後取值,後置會先取值後運算。
如 a++
印出 0
因為先取值;--b
印出 -2
因為先運算。
自增自減運算子還是會做隱式轉換,所以如果丟字串會拿到嘗試轉數字的結果,無法成功轉則拿到 NaN
。
console.log("1"++);
那這個例子呢?答案是會拿到錯誤。
為什麼?因為 ++
和 --
他們只能對可變數值做操作,"1"
本身是個字串,是個不可變類別(原始型別皆是),所以會拋出錯誤:
Uncaught SyntaxError: Invalid left-hand side expression in postfix operation"
那麼這樣呢?
console.log(undefined++);//NaN
console.log(null++);//SyntaxError: Invalid left-hand side expression in postfix operation
跟你預期的一樣嗎?++
做了轉數字的隱式型別轉換,根據規範 undefined
轉為 NaN
,而 null
轉為 0
。
0
是不可變的原始型別 number
,而對 NaN
進行操作只會拿到 NaN
,這就是為什麼這兩行的結果是這樣的。
console.log(Number(undefined));//NaN
console.log(Number(null));//0
+
同時能對數字和文字生效應該雷過不少人。
具體的規則是,從左往右看(在沒有括號的情況下,不考慮運算子優先級),第一個加號的兩邊其中一個運算元是字串,則從此刻開始所有該運算式中的加號做為字串運算子使用。
console.log(5 + 5 + "5");//"105",前面兩個正常加,第二次變字串相加
console.log(5 + "5" + 5);//"555",第一個就開始是字串相加
console.log("5" + 5 + 5);//"555",第一個就開始是字串相加,在左在右沒有差別,都是屬於第一個加的運算元
console.log("5" + 5 * 5);//"525",當有運算子優先級參進來,則高優先級的先處理完,5*5 = 25,再把 5 和 25 做字串相加
所以當要明標該串連加為字串相加,其中一個技巧是在開頭使用""
作為第一個加號的運算元,則整串相加式在該等級的優先度都會被認為是字串相加(如果後面又有括號改變優先級,則括號內依然會先自己處理完)。
console.log("" + 5 + 5 + 5);//"555"
console.log("" + 5 + (5 + 5));//"510",括號內有高優先級,故先處理
加號優先處理字串相加的情況,只有當兩邊皆為數字型別時才會進行數字運算的相加(數字型別包含 bigint
)。
所以如果是其他的型別,如物件,也會被先轉為字串再相加。
console.log({} + {});//"[object Object][object Object]"
console.log(123n+123n);//246n
除加號外,其他算數運算子都會嘗試將運算元轉為數字型別,如:
console.log("1"-0, typeof ("1"-0));//1 number
透過減法做一次 -0
,運算結果就變為了數字。
位元運算子包含 &
,|
,^
,~
,<<
,>>
,>>>
。
位元運算子會把運算元當轉為數值後以 32 位元的集合來看(0 或 1),但回傳時會轉為 JS 中一般十進位的方式顯示。
let a = "1";
let b = 1;
let c = "a";
console.log(a << a);//2
console.log(b << b);//2
console.log(c << 1);//0
console.log(c << 1 | 1);//1
第三個式子為什麼是 0? 因為位元運算一樣會做隱式轉換把參與的運算元轉為數字型別,轉不了的時候一樣是 NaN
,但位元運算會把 NaN
即刻當作 0 來處理。
所以第四個式子即使先前是 NaN
,做 |
運算時一樣能拿到 1
的結果。
另外位元運算子的上限比數字來的低,number
型別是 64 位元的 IEEE 754 雙精度浮點數,而正如上面所說,位元運算子會轉為 32 位元來進行運算,超過 32 位元的都會發出溢位的狀況。
最大值為 2^32 - 1(2,147,483,648),最小值為 -2^31(-2,147,483,648)。
做大數運算的時候要特別小心這點。
~
運算子做的事情是把,每個位元反轉。
console.log(~1);//-2
console.log(~0);//-1
console.log(~-1);//0
有一個比較特別的用法是用於條件判斷。
因為很多找尋式的找不到會回傳 -1
,如 indexOf()。
加上 ~
會把 -1
轉為 0
,而 0
在條件判斷中恰好是個 falsy 的值。
let arr = [1,2,3];
if(~arr.indexOf(4)){console.log("Found");}
else{console.log("Not Found");}
如果沒有 ~
回傳的 -1
是 truthy
,會進到 "Found"
。
但如果有 ~
就能正確顯示 "Not Found"
,是一種相較於 arr.indexOf(4) != -1
更優雅的寫法。(當然需要團隊是能夠理解這樣寫法的意圖的情況)
JS 中僅有一個三元運算子,就是 condition ? exprIfTrue : exprIfFalse
,可以看作是 if...else
的簡寫版。在絕大多數的語言中,三元運算子通常也是作為條件運算子使用。
如果 if
的狀況,只會根據 condition
檢查對應的結果並返回,依然有類似邏輯短路的效果。
let a = {foo:'123'};
console.log(true?a.foo.length:a.bar.length);//3,並不會有錯誤
console.log(false?a.foo.length:a.bar.length);//拋錯
三元是個簡潔的寫法,唯有要避免過度嵌套,或過長的敘述句,就像 if
也往往不會進行過多層寫法(義大利麵程式碼
)。
逗號運算子只有一個 ,
,就像名字一樣。
允許在一個運算式中,運算多個子運算式,並返回最後一個子運算式的結果。
let x = (1,2,3);
console.log(x);//3
常見於 for
迴圈中,用做多個遞增遞減運算式。
let j = 0;
for(let i = 0; i < 5; i++, j++){
console.log(i);
console.log(j);
}
大多數我們只用在這種情況,避免影響可讀性(返回最後一個子運算式的特性),其他狀況我個人傾向於使用多行來表達。
這邊是按 MDN
的分類來的,其實前面我們已經看過很多一元運算子了,包含 ++
或 --
,或正負號等等。
像 typeof
,delete
,void
這些都算是一元運算子。
這些一元運算字看起來像是函式,但其實算做運算子。
特別區分出來是想說運算子不一定只有符號,這些被定義的關鍵字也有可能是運算子。
和函式的不同在於 1. 只能接收一個運算元(一元) 2.無需括號 3.全小寫(對運算子的語言規範,不然以小駝峰 typeof 應該寫成 typeOf)。
關係運算子指的是 in
和 instanceof
。in
用於檢查某個屬性是否存於對象物件中。
let a = {foo:'123'};
console.log('foo' in a);//true
console.log('bar' in a);//false
檢查時需傳入一個能被轉為字串的值作為查詢的屬性名稱,依結果回傳 true
/ false
。
in
在檢查的時候包含其原型鏈上的屬性,且不會特別去管屬性的值是什麼,只處理屬性是否存在。
let a = function(){this.foo = '123'};
let b = new a();
console.log('foo' in b);//true
instanceof
則是用於檢查兩個物件,是否其中一個為另一個的建構對象。
let a = function(){this.foo = '123'};
let b = new a();
console.log(b instanceof a);//true
console.log(b instanceof Object);//true
console.log(b instanceof object);//Uncaught TypeError: Right-hand side of 'instanceof' is not callable"
console.log(b.__proto__ == a.prototype);//true
同樣是經由原型鏈做判斷,如果該物件的建構式存在於原型鏈上,則回傳 true
,否則回傳 false
。
上面把運算子大致介紹告一段落。
來提提運算子優先級吧,在實際世界裡,運算子多為混合使用的,那當混合使用的時候,誰先處理,誰後處理?
最知名的莫過於口訣:先乘除後加減。
console.log( 5 + 5 * 5);//30,因為先 5 * 5
這樣的式子就表明了,作為算術運算子的 *
的優先度是高於 +
的。
MDN 上有個非常充足的表格,請點進去參閱。
裡面寫的相依性和我前文說的結合性是一樣的,同指 Associativity。
結合性負責處理優先序相同的時候運算式的執行方向。
從上面連結的表格可以看到,沒有被歸類在相同優先序但結合性不同的例子(同一個優先序內,總是左結合或右結合,或無結合性)。
表格分得很細,我們抽高一點來看大致的排序,以下為高到低:
()
,括號內必定為最優先,從內部的括號開始往外處理.
[]
,使用或存取優先度僅次於括號,在其他任何運算之前++
--
typeof
!
(包含像 !
這種本來被歸類於邏輯運算子中的)**
優先序最高,再來是 *-/
,最後是 +-
>>
<<
>>>
==
>=
等等和關係運算子們 in
instanceof
&
> ^
> |
&&
> ||
> ??
? :
,所以三元運算常常需要搭配 ()
使用確保表達式,三元返回值都是清楚的,否則可能會被高優先序的運算子影響結果。=
+=
等等其實優先序大致上可以用邏輯來思考,()
第一是沒有爭議的,再來我們需要先取值才能做計算。
做計算的時候必須先處理一元計算子,看看被一元計算子影像後的計算元變成了什麼樣新的值。
接著開始做二元計算,二元計算中四則運算大於位元計算,至此算出了大部分的值,所以我們可以開始做邏輯運算比較值的結果,邏輯比較完可以賦值(返回值)。
最後是逗號運算子可以表示多個子運算式。
比較要特別記得大概是 4 ~ 6 這段,直接對位元做位元數操作優於比較,兩個樹之間做位元操作低於比較。
另外優先序上三元幾乎是最低的,要記得加入對應的括號來適當提升對應式子的優先度,特別是在嵌套的情況下。也可以注意到優先序上,一元高於二元,二元高於三元。
至此將所有的運算子及其相關重要知識大概都已提及。